package tcpclient.v1;

import frame.laxcen.BaseFrame;
import frame.laxcen.response.IResponse;
import frame.laxcen.util.FrameUtil;
import util.ByteBufferUtil;
import util.Ref;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Predicate;

public class RFIDClient {

    private final AsynchronousSocketChannel channel;

    public enum AsyncResult { Success, Interrupted, Timeout, ExecutionError }

    public RFIDClient() throws IOException {

        this.channel = AsynchronousSocketChannel.open();
    }

    public AsyncResult tryConnect(InetSocketAddress address, long millisecond) {

        try {

            channel.connect(address).get(millisecond, TimeUnit.MILLISECONDS);

        } catch (InterruptedException e) {

            e.printStackTrace();

            return AsyncResult.Interrupted;

        } catch (ExecutionException e) {

            e.printStackTrace();

            return AsyncResult.ExecutionError;

        } catch (TimeoutException e) {

            e.printStackTrace();

            return AsyncResult.Timeout;
        }

        return AsyncResult.Success;
    }

    public AsyncResult trySend(byte[] frameData, long millisecond) {

        try {

            channel.write(ByteBuffer.wrap(frameData)).get(millisecond, TimeUnit.MILLISECONDS);

        } catch (InterruptedException e) {

            e.printStackTrace();

            return AsyncResult.Interrupted;

        } catch (ExecutionException e) {

            e.printStackTrace();

            return AsyncResult.ExecutionError;

        } catch (TimeoutException e) {

            e.printStackTrace();

            return AsyncResult.Timeout;
        }

        return AsyncResult.Success;
    }

    public AsyncResult waitForArbitraryResponse(int frameSize, long millisecond, Ref<BaseFrame> response) {

        List<BaseFrame> ref = new ArrayList<>();

        AsyncResult result = waitForResponseList(frameSize, millisecond, ref, (ignored) -> true, 1);

        if(result != AsyncResult.Success && result != AsyncResult.Timeout) return result;

        if(ref.size() >= 1) response.swap(ref.get(0));

        return ref.size() >= 1 ? AsyncResult.Success : AsyncResult.Timeout;
    }

    public AsyncResult waitForSpecialResponse(int frameSize, long millisecond, Ref<BaseFrame> response, Class<? extends IResponse> type) {

        List<BaseFrame> ref = new ArrayList<>();

        AsyncResult result = waitForResponseList(frameSize, millisecond, ref, (frame) -> frame.isResponse(type), 1);

        if(result != AsyncResult.Success && result != AsyncResult.Timeout) return result;

        if(ref.size() >= 1) response.swap(ref.get(0));

        return ref.size() >= 1 ? AsyncResult.Success : AsyncResult.Timeout;
    }

    public AsyncResult waitForResponseList(int frameSize, long millisecond, List<BaseFrame> response) {

        return waitForResponseList(frameSize, millisecond, response, (ignored) -> true, Integer.MAX_VALUE);
    }

    public AsyncResult waitForResponseListSplitByUploadFlag(int frameSize, long millisecond, List<BaseFrame> isNotUpload, List<BaseFrame> isUpload) {

        Predicate<BaseFrame> isUploadFrame = (frame) -> frame.CW.IsUpload;

        return waitForSplitResponseList(frameSize, millisecond, isNotUpload, Integer.MAX_VALUE, isUpload, Integer.MAX_VALUE, isUploadFrame.negate());
    }

    private AsyncResult waitForSplitResponseList(int frameSize, long millisecond, List<BaseFrame> trueResult, int limitOfTrueResult, List<BaseFrame> falseResult, int limitOfFalseResult, Predicate<BaseFrame> predicate) {

        while(millisecond > 0) {

            long begin = System.currentTimeMillis();

            Ref<byte[]> ref = Ref.empty();

            AsyncResult result = waitForByteBlock(frameSize, millisecond, ref);

            millisecond -= System.currentTimeMillis() - begin;

            if(result != AsyncResult.Success && result != AsyncResult.Timeout) return result;

            if(ref.isPresent()) FrameUtil.split(FrameUtil.disjoint(ref.get()), trueResult, falseResult, predicate);

            while(trueResult.size() > limitOfTrueResult) trueResult.remove(trueResult.size() - 1);

            while(falseResult.size() > limitOfFalseResult) falseResult.remove(falseResult.size() - 1);

            if(trueResult.size() >= limitOfTrueResult && falseResult.size() >= limitOfFalseResult) break;
        }

        if(trueResult.size() == 0 && falseResult.size() == 0) return AsyncResult.Timeout;

        return AsyncResult.Success;
    }

    private AsyncResult waitForResponseList(int frameSize, long millisecond, List<BaseFrame> response, Predicate<BaseFrame> predicate, int limit) {

        while(millisecond > 0) {

            long begin = System.currentTimeMillis();

            Ref<byte[]> ref = Ref.empty();

            AsyncResult result = waitForByteBlock(frameSize, millisecond, ref);

            millisecond -= System.currentTimeMillis() - begin;

            if(result != AsyncResult.Success && result != AsyncResult.Timeout) return result;

            if(ref.isPresent()) response.addAll(FrameUtil.disjoint(ref.get()));

            if(response.size() > 0) response.removeIf(predicate.negate());

            if(response.size() >= limit) break;
        }

        if(response.size() <= 0) return AsyncResult.Timeout;

        return AsyncResult.Success;
    }

    private AsyncResult waitForByteBlock(int frameSize, long millisecond, Ref<byte[]> ref) {

        ByteBuffer buffer = ByteBuffer.allocate(frameSize);

        try {

            channel.read(buffer).get(millisecond, TimeUnit.MILLISECONDS);

            ref.swap(ByteBufferUtil.readAndClear(buffer));

        } catch (InterruptedException e) {

            e.printStackTrace();

            return AsyncResult.Interrupted;

        } catch (ExecutionException e) {

            e.printStackTrace();

            return AsyncResult.ExecutionError;

        } catch (TimeoutException e) {

            return AsyncResult.Timeout;
        }

        return AsyncResult.Success;
    }
}
